CSRF
What is a Cross-Site Request Forgery attack and how can you defend your website against it? In this article we're going to implement the double submit cookie pattern in the V programming language to mittigate a CSRF-attack.
What is CSRF?
A Cross-Site Request Forgery attack, or CSRF attack for short, is an attack in which a malicious website, email, blog, message or program causes the user's web browser to perform an action without the user knowing. Often the user is logged in to the website on which the attack is focused. An attacker can make requests to a website through a CSRF attack as if it were the user themselves. The website targeted by the CSRF attack cannot differentiate between a normal user's request and the attacker's request.
Source code
The code used in this article is from the csrf implementation of vweb
. You can find the pull request with all the source code and how to use it in vweb
here.
Reimplementation of vweb's csrf module. Fix for #18099. I did a research project on CSRF for my studies and decided to implement it in V, because the old csrf module is insecure and not up to date ...
https://github.com5 steps of a CSRF attack
In this image you can see a 5 step plan for executing a successful CSRF attack on a banking website, note that this example is massively oversimplified.
- Victim logs in on a website
- The website sends back a session cookie and this cookie gets saved in the browser.
- The attacker "forges" a request, this request is mostly in the form of an URL and sends this to the victim. This step usually requires extensive knowledge and recon of the target's website.
- If the victim gets tricked by the attacker their browser will request the forged URL. The cookies for the target website get sent automatically with the request.
- The target validates the cookie and processes the forged request.
The danger of a CSRF-attack
The actual danger of a CSRF-attack is that the target website couldn't distinguish the forged request from a real request. A successful CSRF-attack gets noticed only when the victim notices the consequences. For authentication-sensitive applications this is a nightmare scenario.
Defences
There are a couple of methods known to defend against a CSRF-attack according to OWASP. Each method has its own pros and cons. But in this article we are going to implement the double submit cookie pattern in the V programming language.
The App
Alright lets code! We are going to make a very basic html profile page where a logged in user can change their name. To test the app you will need to set a session cookie: session_id=1
.
First we need to make a vweb
App that will serve as a basis for our app.
main.v
module main
import vweb
struct App {
vweb.Context
}
pub fn (mut app App) index() vweb.Result {
return $vweb.html()
}
[post]
pub fn (mut app App) change_name() vweb.Result {
// check the session id cookie
session_id := app.get_cookie('session_id') or { '' }
if session_id != '1' {
app.set_status(401, '')
return app.text('HTTP 401: Unauthorized')
}
name := app.form['name']
return $vweb.html()
}
fn main() {
vweb.run(&App{}, 8080)
}
And the corresponding html pages:
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
</head>
<body>
<h1>Profile</h1>
<p>Update your profile name:</p>
<form action="/change_name" method="post">
<input type="text" name="name" placeholder="John Doe"/>
<input type="submit"/>
</form>
</body>
</html>
templates/change_name.html
<!DOCTYPE html>
<html>
<head>
<title>Name</title>
</head>
<body>
<h1>Hello @name</h1>
</body>
</html>
If you run the app with v run .
and visit http://localhost:8080 you will see a simple HTML page. After I fill in my name and submit the form, I can see a page with the text "Hello Casper"
.
Exploit the CSRF vulnerability
This app is not protected against CSRF-attacks and we could exploit it by making a fake web page and some javascript. If an attacker would successfully trick a victim into opening that webpage their cookies will be send and their name would be changed without them knowing!
<!DOCTYPE html>
<html>
<head>
<title>Exploit</title>
</head>
<body>
<h1>Evil website</h1>
</body>
<script>
const data = new FormData();
data.append("name", "Attacker");
// send a POST request to the url of our website.
fetch("http://localhost:8080/change_name", {
method: "post",
body: data
})
</script>
</html>
This is ofcourse a gross oversimplification of what is possible with a CSRF-attack.
Adding CSRF protection
The double submit cookie method protects against CSRF because it sets two tokens: one anti-csrf-oken is set as a hidden input in a form and the other is set as a cookie. The token in the cookie is actually the hmac of the anti-csrftoken. The anti-csrftoken contains the session id. This ensures that the token is cryptographically bound to the users session id.
Getting the token
With vweb's CSRF module we can easily get the csrftoken in a route and protect it. See the documentation for the configuration options.
main.v
// add the import and const at the top of your file
import vweb.csrf
const (
csrf_config := csrf.CsrfConfig{
// change the secret
secret: 'my-secret'
// change to which domains you want to allow
allowed_hosts: ['*']
// the name of the session id cookie
session_cookie: 'session_id'
}
pub fn (mut app App) index() vweb.Result {
// get the token and set the csrf cookie
csrftoken := csrf.set_token(mut app.Context, csrf_config)
return $vweb.html()
}
Add this input to the form in templates/index.html
<input type="hidden" name="csrftoken" value="@csrftoken"/>
If you visit the page in your browser a cookie with the name "csrftoken"
will be set and the form contains a hidden input with the anti-csrftoken.
Protecting a route
To protect a route simply call csrf.protect at the beginning of the handler.
[post]
pub fn (mut app App) change_name() vweb.Result {
// protect the route
csrf.protect(mut app.Context, csrf_config)
// check the session id cookie
session_id := app.get_cookie('session_id') or { '' }
if session_id != '1' {
app.set_status(401, '')
return app.text('HTTP 401: Unauthorized')
}
name := app.form['name']
return $vweb.html()
}
If you make a post request to /change_name
without any cookies we now get an http 401 response! You will get the same response if your cookies are valid, but there is no token present in the form data.
Other usages
Middleware
You can also use vweb's middleware to protect multiple routes at once.
main.v
// The rest is the same
struct App {
vweb.Context
pub mut:
middlewares map[string][]vweb.Middleware
}
fn main() {
app := &App{
middlewares: {
// protect all routes starting with the url '/change_name'
'/change_name': [csrf.middleware(csrf_config)]
}
}
vweb.run(app, 8080)
}
[post]
pub fn (mut app App) change_name() vweb.Result {
// protect the route
csrf.protect(mut app.Context, csrf_config)
// check the session id cookie
session_id := app.get_cookie('session_id') or { '' }
if session_id != '1' {
app.set_status(401, '')
return app.text('HTTP 401: Unauthorized')
}
name := app.form['name']
return $vweb.html()
}
CsrfApp
Or you can use the CsrfApp
struct if you want the protection to be available inside your App
struct.
module main
import net.http
import vweb
import vweb.csrf
struct App {
vweb.Context
pub mut:
csrf csrf.CsrfApp [vweb_global]
}
fn main() {
app := &App{
csrf: csrf.CsrfApp{
// change the secret
secret: 'my-secret'
// change to which domains you want to allow
allowed_hosts: ['*']
// the name of the session id cookie
session_cookie: 'session_id'
}
}
vweb.run(app, 8080)
}
pub fn (mut app App) index() vweb.Result {
// get the token and set the csrf cookie
csrftoken := app.csrf.set_token(mut app.Context)
return $vweb.html()
}
[post]
pub fn (mut app App) change_name() vweb.Result {
// protect the route
app.csrf.protect(mut app.Context)
// check the session id cookie
session_id := app.get_cookie('session_id') or { '' }
if session_id != '1' {
app.set_status(401, '')
return app.text('HTTP 401: Unauthorized')
}
name := app.form['name']
return $vweb.html()
}
Source Code
See vlib/vweb/csrf for the source code.